Skip to content
On this page

側邊欄功能實現

實現概念說明

  • 畫面呈現上會有主選單列"HeaderMenu"、頁面內容區"Content"、側邊欄頁面切換"SplitView.Pane"、底部資訊欄"Footer"
  • 使用SplitView這元件來實現側邊欄效果,這元件主要分為"<SplitView.Pane>"、"<SplitView.Content>"分別負責側邊欄顯示與頁面內容顯示
  • <SplitView.Pane> 欄位可以由各種元件組成,這邊採用的是 ListBox來實現選單按鍵
  • 此篇文章聚焦在"基礎版型實現"、"側邊欄開關"、"頁面切換"功能實現

本章範例下載

此篇當作筆記看看就好,紀錄到後面自己也有些亂掉了,未來有機會時再找機會重新整理

一、基礎版型實現

# Avalonia側邊欄基礎架構示意

二、HeaderMenu

編輯 Views 目錄下的 MainWindow.axaml,這是 MVVM 樣板預設的主視窗。我們首先定義一個 Grid 作為整體的佈局框架,並設定 RowDefinitions="Auto, *" 來規劃垂直空間。這代表將視窗切分為上下兩列:第一列的高度設為 Auto(隨內容自動調整)=>HeaderMenu高度,而第二列的高度則設為 *(自動填滿視窗剩餘的所有空間)=>側邊欄高度。

xml
<!-- 主視窗框架區塊 -->
<Grid RowDefinitions="Auto, *">

</Grid>

完成主視窗的網格佈局 (Grid Layout) 設定後,我們就能開始填入 UI 元件。首先,在畫面上方加入一個 Menu 作為功能列。

這裡有兩個關鍵設定:首先透過 Grid.Row="0" 將HeaderMenu定位於我們剛剛定義的第一列(即高度為 Auto 的區域),接著設定 Background 屬性來定義HeaderMenu的背景色。而在 Menu 內部,我們使用 MenuItem 來建立具體的選單選項,並透過 Header 屬性來設定顯示的文字或符號(例如漢堡圖示 "☰")。

xml
<!-- 主視窗框架區塊 -->
<Grid RowDefinitions="Auto, *">
    <!-- Header Menu-->
    <Menu Background="#ECF0F1" Grid.Row="0">
        <MenuItem Header=""/>
        <MenuItem Header="關於">
            <MenuItem Header="關於應用" />
        </MenuItem>
    </Menu>
</Grid>

# HeaderMenu實現

三、實作側邊欄 (Sidebar) 的顯示與隱藏

這邊主要分為 View 與 ViewModel 兩部分的整合:

首先在 View (MainWindow) 加入 SplitView 控制項。我們設定 Grid.Row="1" 讓側邊欄顯示於 Header Menu 下方,並將 DisplayMode 設為 "Overlay" (覆蓋模式),OpenPaneLength 設定為 200 以定義展開寬度。

最關鍵的屬性是 IsPaneOpen,我們將其雙向綁定 (TwoWay) 至 ViewModel 的 IsSidebarOpen 屬性:

xml
IsPaneOpen="{Binding IsSidebarOpen, Mode=TwoWay}"

如此一來,側邊欄的顯示狀態就完全由變數決定。

接著在 ViewModel (MainWindowViewModel.cs) 中建立 _isSidebarOpen 變數,並實作 ToggleSidebar 邏輯與 ToggleSidebarCommand

最後,將介面上的漢堡選單按鈕 (MenuItem Header="☰") 綁定至 ToggleSidebarCommand。每當按鈕被點擊,Command 便會翻轉 IsSidebarOpen 的布林值,進而驅動 UI 的 IsPaneOpen 屬性,完成側邊欄的開關切換。

xml
MainWindow.axml
<!-- 主視窗框架區塊 -->
<Grid RowDefinitions="Auto, *">
    <!-- Header Menu-->
    <Menu Background="#ECF0F1" Grid.Row="0">
        <MenuItem Header="" Command="{Binding ToggleSidebarCommand}"/>
        <MenuItem Header="關於">
            <MenuItem Header="關於應用" />
        </MenuItem>
    </Menu>

    <!--側邊欄-->
    <SplitView Grid.Row="1"
               DisplayMode="Overlay"
               OpenPaneLength="200"
               IsPaneOpen="{Binding IsSidebarOpen, Mode=TwoWay}">
        <SplitView.Pane>
            <Border BorderBrush="#BDC3C7"
                    BorderThickness="0, 0, 1, 0">
                <StackPanel Background="#ECF0F1">
                    <TextBlock  Text="導航選單"
                                FontSize="16"
                                HorizontalAlignment="Center"
                                Margin="15, 20, 15, 10"/>

                </StackPanel>
            </Border>
        </SplitView.Pane>
    </SplitView>

</Grid>

在 ViewModel 的實作部分,我們使用 MVVM Toolkit 來簡化程式碼。透過 [ObservableProperty] 標籤修飾私有變數,套件便會自動產生可供 View 綁定的公開屬性與通知機制;而在函數上方加上 [RelayCommand] 標籤後,系統則會自動生成對應的 Command(例如 ToggleSidebar 會生成 ToggleSidebarCommand),讓 View 端的元件可以直接呼叫使用。

C#
MainWindowViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace AvaloniaSideBarEx.ViewModels;

public partial class MainWindowViewModel : ViewModelBase
{
    [ObservableProperty]
    private bool _isSidebarOpen;

    [RelayCommand]
    private void ToggleSidebar()
    {
        IsSidebarOpen = !IsSidebarOpen;
    }
}

# 側邊欄範例效果.png

四、實作側邊欄 (Sidebar) 的頁面切換

頁面準備 View/ViewModel

  1. 首先在Views增加3個測試用的頁面(UserControl),AboutView、HomeView、SettingsView。 新增View.png

  2. 除了新增View之外還需要先建立好View所對應的ViewModel,並先簡單建立歡迎訊息 新增ViewModel.png

    ViewModel修改時除了要寫變數之外還要記得繼承 ViewModelBase ,這樣後續才可存入ObservableObject ViewModel修改.png

  3. 讓View與ViewModel來綁定,其中的關鍵就是xmlns:vm="clr-namespace:AvaloniaSideBarEx.ViewModels" x:DataType="vm:HomeViewModel",如此一來在View就可以使用ViewModel了

xml
    <UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:AvaloniaSideBarEx.ViewModels"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:DataType="vm:HomeViewModel"
             x:Class="AvaloniaSideBarEx.Views.HomeView">
        <StackPanel Margin="20" Spacing="15">
            <TextBlock  Text="{Binding WelcomeMessage}"
                        FontSize="16"
                        Foreground="#34495e"/>
        </StackPanel>
    </UserControl>

建立DI容器提供服務

  1. NuGet來安裝DI套件 Microsoft.Extensions.DependencyInjection
  2. 修改App.axaml.cs (定義 ServiceProvider、初始化 DI 容器、建立 Provider、解析 MainWindow 並注入 ViewModel)
C#
  using System;
  using Avalonia;
  using Avalonia.Controls.ApplicationLifetimes;
  using Avalonia.Data.Core;
  using Avalonia.Data.Core.Plugins;
  using System.Linq;
  using Avalonia.Markup.Xaml;
  using AvaloniaSideBarEx.ViewModels;
  using AvaloniaSideBarEx.Views;
  using Microsoft.Extensions.DependencyInjection;

  namespace AvaloniaSideBarEx;

  public partial class App : Application
  {
      // 定義 ServiceProvider
      public IServiceProvider? Services { get; set; }

      public override void Initialize()
      {
          AvaloniaXamlLoader.Load(this);
      }
      public override void OnFrameworkInitializationCompleted()
      {
          // 初始化 DI 容器
          var collection = new ServiceCollection();
          // 註冊 ViewModels
          collection.AddSingleton<MainWindowViewModel>();
          collection.AddSingleton<HomeViewModel>();
          collection.AddSingleton<SettingsViewModel>();
          collection.AddSingleton<AboutViewModel>();

          // 建立 Provider
          Services = collection.BuildServiceProvider();
          if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
          {
              // 解析 MainWindow 並注入 ViewModel
              var mainVm = Services.GetRequiredService<MainWindowViewModel>();
              DisableAvaloniaDataAnnotationValidation();
              desktop.MainWindow = new MainWindow
              {
                  DataContext = mainVm
              };
          }
          base.OnFrameworkInitializationCompleted();
      }
      ...略

側邊欄導航邏輯實作 (Navigation Logic)

為了實現側邊欄的頁面切換與資源管理,我們需要對 MainWindowViewModel 進行重構,主要分為「依賴注入設定」與「導航邏輯實作」兩個部分。 在 MainWindowViewModel 中,我們實作了側邊欄的導航邏輯。主要目標是:當使用者選擇選單項目時,能夠切換顯示對應的 ViewModel,同時確保頁面狀態不會因為切換而消失(例如在設定頁輸入的內容切換回來後還在)。

1. 基礎設置:依賴注入與繼承

首先,修改 MainWindowViewModel,改用 C# 12 的 Primary Constructor (主要建構函數) 語法,並透過建構式注入 (Constructor Injection) 傳入 IServiceProvider 以便後續取得服務。 同時,別忘了繼承 ViewModelBase 以確保 MVVM 功能運作正常:

C#
public partial class MainWindowViewModel(IServiceProvider services) : ViewModelBase
{
    // ... 內部的程式碼
}

有了這個 services 變數後,我們就能在接下來的邏輯中,動態向系統請求所需的 ViewModel 實例。

2. 頁面快取與選單定義

首先,我們宣告了 _homeViewModel_settingsViewModel 等私有欄位,這些變數用於 快取 (Cache) 已經實例化的頁面。這樣當使用者在頁面間來回切換時,我們不需要每次都重新建立新的 ViewModel,從而保留頁面操作狀態並節省效能。 同時,定義 MenuItems 列表(包含 "首頁"、"設定"、"關於"),供前端 View 的 ListBox 綁定顯示。

3. 狀態綁定 (Observable Properties)

我們定義了兩個關鍵的可觀察屬性:

  • SelectedMenuItem:綁定到側邊欄選單的選擇項,用來追蹤使用者點了哪個按鈕。
  • CurrentPage:這是導航的核心,它代表目前右側主畫面要顯示的內容。初始值我們透過 services.GetRequiredService<HomeViewModel>() 直接載入首頁,確保程式一啟動就有畫面。
4. 觸發導航 (Command & Interaction)

透過 [RelayCommand] 建立 OnMenuSelected 函數。當 View 端的選單選項改變時(透過綁定觸發),此函數會執行:

  1. 呼叫 Navigate() 進行頁面切換。
  2. 判斷 IsSidebarOpen,如果在開啟狀態下點擊了選項,則自動關閉側邊欄,提供更流暢的使用者體驗 (UX)。
5. 導航方法 (Navigate with Lazy Loading)

Navigate 方法使用了 C# 的 switch 表達式搭配 空值合併指派運算子 (??=) 來實現 懶加載 (Lazy Loading) 機制:

C#
"首頁" => _homeViewModel ??= services.GetRequiredService<HomeViewModel>()

這段語法的含義是:「檢查 _homeViewModel 是否已經有值?如果有,直接使用它(快取);如果沒有(即第一次開啟該頁面),則向 DI 容器請求一個新的實例並存入變數中。」

透過這種方式,我們實現了按需載入(On-Demand Loading),既優化了記憶體使用,又確保了頁面狀態的連續性。

C#
MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;

namespace AvaloniaSideBarEx.ViewModels;

public partial class MainWindowViewModel(IServiceProvider services) : ViewModelBase
{
    //側邊欄控制
    [ObservableProperty]
    private bool _isSidebarOpen;

    [RelayCommand]
    private void ToggleSidebar()
    {
        IsSidebarOpen = !IsSidebarOpen;
    }

    //側邊欄導航
    private HomeViewModel? _homeViewModel;
    private SettingsViewModel? _settingsViewModel;
    private AboutViewModel? _aboutViewModel;
    public List<string> MenuItems { get; } = ["首頁", "設定", "關於"];

    [ObservableProperty]
    private string? _selectedMenuItem = "首頁";

    [ObservableProperty]
    private ObservableObject? _currentPage = services.GetRequiredService<HomeViewModel>();

    // 這是 CommunityToolkit 自動生成的鉤子方法 (Hook)
    // 當 _selectedMenuItem 改變時,這個函數會被自動呼叫
    partial void OnSelectedMenuItemChanged(string? value)
    {
        // 直接在這裡呼叫你的導航邏輯
        OnMenuSelected();
    }

    [RelayCommand]
    private void OnMenuSelected()
    {
        //Debug.WriteLine($"當前選中:{SelectedMenuItem}");
        Navigate(SelectedMenuItem);
        if (IsSidebarOpen)
        {
            IsSidebarOpen = false;
        }
    }

    private void Navigate(string? target)
    {
        //根據選中的菜單名稱,實例化對應的 ViewModel
        CurrentPage = target switch
        {
            "首頁" => _homeViewModel ??= services.GetRequiredService<HomeViewModel>(), // 如果 _homeViewModel 是 null,則創建新的並賦值
            "設定" => _settingsViewModel ??= services.GetRequiredService<SettingsViewModel>(),
            "關於" => _aboutViewModel ??= services.GetRequiredService<AboutViewModel>(),
            _ => _homeViewModel ??= services.GetRequiredService<HomeViewModel>()
        };
    }
}
xml
MainWindow.axml

<SplitView.Pane>
    <Border BorderBrush="#BDC3C7"
            BorderThickness="0, 0, 1, 0">
        <StackPanel Background="#ECF0F1">
            <TextBlock  Text="導航選單"
                        FontSize="16"
                        HorizontalAlignment="Center"
                        Margin="15, 20, 15, 10"/>
            <ListBox Background="Transparent"
                     BorderThickness="0"
                     Padding="0"
                     SelectionMode="Single"
                     ItemsSource="{Binding MenuItems}"
                     SelectedItem="{Binding SelectedMenuItem, Mode=TwoWay}"
                     SelectedIndex="0">
                    <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock  Text="{Binding }"
                                    Margin="3"
                                    HorizontalAlignment="Center"/>
                    </DataTemplate>
                 </ListBox.ItemTemplate>
            </ListBox>
        </StackPanel>
    </Border>
</SplitView.Pane>

<SplitView.Content>
    <Grid RowDefinitions="*, Auto" ColumnDefinitions="*, *, *" Background="#ECF0F1">
        <ContentControl Grid.Row="0"
                        Grid.ColumnSpan="3"
                        Content="{Binding CurrentPage}"
                        VerticalAlignment="Stretch"
                        HorizontalAlignment="Stretch"/>
        <Border Grid.Column="0"
                Grid.Row="1"
                Grid.ColumnSpan="3"
                BorderBrush="#BDC3C7"
                BorderThickness="0, 1, 0, 0"
                Padding="10">
            <TextBlock  Text="Xian - 版權所有 © 2025"HorizontalAlignment="Right"
                        FontSize="12"/>
        </Border>
    </Grid>
</SplitView.Content>

未經授權請勿進行全文複製或商業用途。
Content licensed under CC BY-NC-ND 4.0.